yo, what's up?
Product types 允許同時存在兩種以上的資料型態在內
舉例來說現在我們建立一個特別的型別叫 Clock
,其可以放入兩個數值,Hour
與 Period
class Clock {
constructor(hour, period) {
if(!Array.from({length: 12}, (_, i) => i + 1).includes(hour) || !['AM', 'PM'].includes(period)){
throw new Error('format error!')
}
this.hour = hour;
this.period = period;
}
}
大家可以思考一下,我們呼叫 Clock 可能的組合會有多少?
const One_AM = new Clock(1, "AM");
const Two_AM = new Clock(2, "AM");
// ... 以此類推
沒錯,是 24 種,因為 Hour 有 12 種, Period 則是 2 種,所以 Clock 會有 12 * 2 = 24 種不同的組合。
用在數學世界中,通常會將其表示
C([A, B]) = C(A) * C(B)
C(A): 為 type A
有多少種元素在內。例如 Hour
有 12 種。
而 Product type 運用的時機通常為其組成數值為相互獨立的。 像是 hour 改變時, period 並不會受到影響。
Sum types 則每次只能有一組固定的資料型態
用在數學世界中,通常會將其表示
C(A | B) = C(A) + C(B)
例如 TODO List 其狀態包含 CRUD, 並且會用 type 去標記現在的狀態 (ex CREATE, REMOVE)
const CREATE_TODO = 'CREATE_TODO'
const REMOVE_TODO = 'REMOVE_TODO'
{
type: CREATE_TODO,
text
}
{
type: REMOVE_TODO,
id,
}
而每個 Action 都會有自己的 constructors
const create = (text) => ({
type: CREATE_TODO,
text
})
const remove = (id) => ({
type: REMOVE_TODO,
id
})
這就是 Sum Type 的概念, Action 裡面只會有一種動作,不會同時有多個,而每個動作都會用 tag
去標記,這也讓我們可以對其進行 recursive,舉 LinkedList 為例,
如果用 TS 的 interface 表達
type LinkedList<A> =
| { readonly _tag: 'Nil' }
| { readonly _tag: 'Cons'; readonly head: A; readonly tail: LinkedList<A> }
而 LinkedList<A>
就是 recursion.
在一些 FP 語言中,有pattern matching 這個非常好用的功能。而 JavaScript 則有相關的 proposal 正在進行,但在原生沒有這個功能前,我們可以實作出一個類似的 pattern matching 的函式 match
.
繼續沿用 LinkedList 作為範例,
const nil = { _tag: 'Nil' };
const cons = (head, tail) => ({
_tag: 'Cons',
head,
tail,
});
const match = (onNil, onCons) => (fa) => {
switch (fa._tag) {
case 'Nil':
return onNil();
case 'Cons':
return onCons(fa.head, fa.tail);
}
};
// 此 LinkedList 是否為空
const isEmpty = match(
() => true,
() => false
);
// 新增 item 在 LinkedList 中
const addFirst = (num) =>
match(
() => cons(num, nil),
(head, _tail) => cons(num, cons(head, _tail))
);
// 取得該 LinkedList 第一個數值
const head = match(
() => undefined,
(head, _tail) => head
);
// 取得該 LinkedList 最後一個數值
const last = match(
() => undefined,
(head, tail) => (tail._tag === 'Nil' ? head : last(tail))
);
// 取得該 LinkedList 長度
const length = match(
() => 0,
(_, tail) => 1 + length(tail)
);
const myList = cons(1, cons(2, cons(3, nil)));
isEmpty(myList) // false
如果兩值(狀態)相依時,以 react 為例,我們常常會寫出類似這樣的程式
import React, { useState, useEffect } from 'react';
const App = () => {
const [loading, setLoading] = useState(false)
const [error, setError] = useState(null);
const [data, setData] = useState(null);
...
return <>{!loading && !error && data.map(/** rendering */)}</>
}
而這種兩值相依的情況就非常適合使用 Sum type,我們就來將上面改寫一下,
首先我們先定義其可能的狀態,並根據每個狀態給定 constructor.
const match = (onInit, onLoading, onError, onSuccess) => (fa) => {
switch (fa._tag) {
case 'INIT':
return onInit();
case 'LOADING':
return onLoading();
case 'ERROR':
return onError(fa.error);
case 'SUCCESS':
return onSuccess(fa.data);
default:
break;
}
};
const STATE = {
INIT: { _tag: 'INIT' },
LOADING: { _tag: 'LOADING' },
ERROR: (error) => ({
_tag: 'ERROR',
error,
}),
SUCCESS: (data) => ({ _tag: 'SUCCESS', data }),
};
接下來進行 fetch 以及 UI render
import React, { useEffect, useState } from 'react';
export default function App() {
const [result, setResult] = useState(STATE.INIT);
useEffect(() => {
const runEffect = () => {
setResult(STATE.LOADING);
fetch('https://jsonplaceholder.typicode.com/todos')
.then((response) => response.json())
.then((data) => setResult(STATE.SUCCESS(data)))
.catch((error) => setResult(STATE.ERROR(error)))
};
runEffect();
}, []);
const renderer = match(
() => <div>initial...</div>,
() => <div>loading...</div>,
(error) => <div>{JSON.stringify(error)}</div>,
(xs) =>
xs.map((x) => (
<code key={x.id}>
<pre>{JSON.stringify(x, null, 2)}</pre>
</code>
))
);
return <>{renderer(result)}</>;
}
Product Type 適合用在兩值相互獨立的情況, Sum Type 則適合用在兩值相依的情況,而 ADT 的概念的應用在處理業務邏輯上。
NEXT: Semigroup